from chimera.core.remoteobject import RemoteObject
import database as db
from datetime import datetime
import matplotlib.pyplot as plt
from types import StringTypes
try:
    import cStringIO as StringIO
except ImportError:
    import StringIO

def _fitField(string):
    if len(string) > 15:
        return string[0 : 15] + " "
    else:
        return string +  " " * (16 - len(string))

def getFields(data, field):
    """
    When given a list of datamodel objects and a field name, returns a list of
    the corresponding field values from those objects.
    """

    results = []
    for d in data:
        results.append(d.getField(field))
    return results

def printFields(data, field):
    """
    When given a list of datamodel objects and a field name, display a list of
    the corresponding field values from those objects.
    """

    for d in data:
        print d.getField(field)

def printMultipleFields(data, fields):
    """
    When given a list of datamodel objects and a list of field names, displays a
    table of the corresponding field values from those objects.
    """

    line = ""
    for f in fields:
        line += _fitField(str(f))
    print line

    for d in data:
        line = ""
        for f in fields:
            line += _fitField(str(d.getField(f)))
        print line

class _Filter:
    def __init__(self, table, field, op, criteria):
        self.table = table
        self.field = field
        self.op = op
        self.criteria = criteria

class _Column:
    def __init__(self, name, batch, table, field):
        self.name = name
        self.batch = batch
        self.table = table
        self.field = field
        self.data = []
        self._dataObsDate = []
        self._dataIdx = 0

class _Batch:
    def __init__(self, name, table = None, field = None, op = None, criteria = None):
        self.name = name
        if table != None:
            self.definition = _Filter(table, field, op, criteria)
        else:
            self.definition = None
        self.filters = []
        self.results = []

class DataView(RemoteObject):
    """
    This class is an alternative wrapper for the querying functionality of the
    database.

    To use the DataView class, you first select the table that will be the base
    of the query (ExposureStar by default). Next, you can choose to filter the
    contents of the database and group them together in various ways. Next, you
    choose the columns of data to use, fill those columns with the query
    results, and output those results as a table or plot.

    The DataView class uses the concept of batching to enable more kinds of
    queries. Essentially, batching allows data from successive images to be
    displayed in the same row, allowing for comparisons between stars, or
    displaying data about the same star collected through images in different
    filters.

    Most of the functions request a field name. This is expressed as a string
    using the following dot notation: "batch_name.table_name.field_name" or just
    "table_name.field_name", where batch_name is the name of a batch already
    added, table_name is a member of the Tables enum in datamodel, and
    field_name is a member of the StarFields, ExposureFields, or ExpStarFields
    enum in datamodel.
    """

    def __init__(self, database = None):
        RemoteObject.__init__(self)
        
        if database == None:
            self.database = db.Database()
        else:
            self.database = database
        self.base = db.Tables.EXPSTAR
        self.filters = []
        self.defbatch = _Batch("", None, None, None, None)
        self.batches = []
        self.columns = []
    
    def _parseFieldName(self, fieldname):
        # Possible formats:
        # BATCH.TABLE.FIELD
        # BATCH.FIELD       (TABLE set to 'EXPSTAR')
        # TABLE.FIELD       (BATCH set to self.defbatch)
        # FIELD             (BATCH set to self.defbatch, TABLE set to 'EXPSTAR')
        
        names = fieldname.split('.')
        if len(names) == 3:
            batch = None
            for b in self.batches:
                if b.name == names[0]: batch = b
            if batch == None: raise Exception("Batch does not exist: %s" % (names[0]))
            table = names[1]
            field = names[2]
        elif len(names) == 2:
            batch = self.defbatch
            for b in self.batches:
                if b.name == names[0]: batch = b
            if batch == self.defbatch: table = names[0]
            else:                      table = 'EXPSTAR'
            field = names[1]
        else:
            batch = self.defbatch
            table = 'EXPSTAR'
            field = names[0]

        if table in db.Tables:
            table = db.Tables.fromStr(table)
        else:
            raise Exception("Table does not exist: %s" % (table))

        if (table == db.Tables.EXPSTAR) and (field in db.ExpStarFields):
            field = db.ExpStarFields.fromStr(field)
        elif (table == db.Tables.STAR) and (field in db.StarFields):
            field = db.StarFields.fromStr(field)
        elif (table == db.Tables.EXPOSURE) and (field in db.ExposureFields):
            field = db.ExposureFields.fromStr(field)
        elif (table == db.Tables.EXTINCTION) and (field in db.ExtinctionFields):
            field = db.ExtinctionFields.fromStr(field)
        else:
            raise Exception("Field does not exist: %s" % (field))

        return batch, table, field

    def setBase(self, table):
        oldBase = self.base

        if table in db.Tables:
            self.base = db.Tables.fromStr(table)
        else:
            raise Exception("Table does not exist: %s" % (field))

        return oldBase

    def addColumn(self, name, fieldname):
        # Parse fieldname into batch, table, and field
        batch, table, field = self._parseFieldName(fieldname)

        # Add to columns
        self.columns.append(_Column(name, batch, table, field))

        return None

    def batchAll(self, name):
        # Add to batch filters or universal filters
        self.batches.append(_Batch(name))

        return None        

    def batchExact(self, name, fieldname, value):
        # Parse fieldname into batch (optional), table, and field
        batch, table, field = self._parseFieldName(fieldname)

        # Add to batch filters or universal filters
        self.batches.append(_Batch(name, table, field, db.FilterOps.EXACT, value))

        return None

    def batchMax(self, name, fieldname, value):
        # Parse fieldname into batch (optional), table, and field
        batch, table, field = self._parseFieldName(fieldname)

        # Add to batch filters or universal filters
        self.batches.append(_Batch(name, table, field, db.FilterOps.MAX, value))

        return None

    def batchMin(self, name, fieldname, value):
        # Parse fieldname into batch (optional), table, and field
        batch, table, field = self._parseFieldName(fieldname)

        # Add to batches
        self.batches.append(_Batch(name, table, field, db.FilterOps.MIN, value))

        return None

    def batchRange(self, name, fieldname, min, max):
        # Parse fieldname into batch (optional), table, and field
        batch, table, field = self._parseFieldName(fieldname)

        # Swap if necessary
        if max < min: temp = max; max = min; min = temp
        
        # Add to batches
        batch = _Batch(name, table, field, db.FilterOps.MIN, min)
        self.batches.append(batch)
        batch.filters.append(_Filter(table, field, db.FilterOps.MAX, max))

        return None

    def batchError(self, name, fieldname, value, error):
        # Parse fieldname into batch (optional), table, and field
        batch, table, field = self._parseFieldName(fieldname)

        # Add to batches
        batch = _Batch(name, table, field, db.FilterOps.MIN, value - error)
        self.batches.append(batch)
        batch.filters.append(_Filter(table, field, db.FilterOps.MAX, value + error))

        return None

    def filterExact(self, fieldname, value):
        # Parse fieldname into batch (optional), table, and field
        batch, table, field = self._parseFieldName(fieldname)
        
        # Add to batch filters or global filters
        if batch == None:
            self.filters.append(_Filter(table, field, db.FilterOps.EXACT, value))
        else:
            batch.filters.append(_Filter(table, field, db.FilterOps.EXACT, value))

        return None

    def filterMax(self, fieldname, value):
        # Parse fieldname into batch (optional), table, and field
        batch, table, field = self._parseFieldName(fieldname)

        # Add to batch filters or global filters
        if batch == None:
            self.filters.append(_Filter(table, field, db.FilterOps.MAX, value))
        else:
            batch.filters.append(_Filter(table, field, db.FilterOps.MAX, value))

        return None

    def filterMin(self, fieldname, value):
        # Parse fieldname into batch (optional), table, and field
        batch, table, field = self._parseFieldName(fieldname)

        # Add to batch filters or global filters
        if batch == None:
            self.filters.append(_Filter(table, field, db.FilterOps.MIN, value))
        else:
            batch.filters.append(_Filter(table, field, db.FilterOps.MIN, value))

        return None

    def filterRange(self, fieldname, min, max):
        # Parse fieldname into batch (optional), table, and field
        batch, table, field = self._parseFieldName(fieldname)

        # Swap if necessary
        if max < min: temp = max; max = min; min = temp

        # Add to batch filters or global filters
        if batch == None:
            self.filters.append(_Filter(table, field, db.FilterOps.MIN, min))
            self.filters.append(_Filter(table, field, db.FilterOps.MAX, max))
        else:
            batch.filters.append(_Filter(table, field, db.FilterOps.MIN, min))
            batch.filters.append(_Filter(table, field, db.FilterOps.MAX, max))

        return None

    def filterError(self, fieldname, value, error):
        # Parse fieldname into batch (optional), table, and field
        batch, table, field = self._parseFieldName(fieldname)

        # Add to batch filters or global filters
        if batch == None:
            self.filters.append(_Filter(table, field, db.FilterOps.MIN, value - error))
            self.filters.append(_Filter(table, field, db.FilterOps.MAX, value + error))
        else:
            batch.filters.append(_Filter(table, field, db.FilterOps.MIN, value - error))
            batch.filters.append(_Filter(table, field, db.FilterOps.MAX, value + error))

        return None

    def fillColumns(self):
        """
        Data is returned as array of column arrays.
        """

        # Process batches
        for batch in self.batches + [self.defbatch]:
            # Start query
            if self.base == db.Tables.EXPSTAR:
                q = db.ExposureStar.query.join(db.ExposureStar.exposure).join(db.ExposureStar.star)
            elif self.base == db.Tables.EXPOSURE:
                q = db.Exposure.query
            elif self.base == db.Tables.STAR:
                q = db.Star.query

            # Process filters
            for f in self.filters + self.defbatch.filters + batch.filters + [batch.definition]:
                if f == None: # Batch definition filters may be None
                    pass
                elif f.table == db.Tables.EXPSTAR:
                    q = db.ExposureStar.filterQuery(q, f.field, f.op, f.criteria)
                elif f.table == db.Tables.STAR:
                    q = db.Star.filterQuery(q, f.field, f.op, f.criteria)
                elif f.table == db.Tables.EXPOSURE:
                    q = db.Exposure.filterQuery(q, f.field, f.op, f.criteria)

            # Sort results by time
            if (self.base == db.Tables.EXPSTAR) or (self.base == db.Tables.EXPSTAR):
                q = q.order_by(db.desc(db.Exposure.obsdate))

            # Gather query results
            batch.results = q.all()

        # Assemble table
        for col in self.columns:
            col.data = []
            for x in col.batch.results:
                if self.base == db.Tables.EXPSTAR:
                    if col.table == db.Tables.EXPSTAR:
                        col.data.append(x.getField(col.field))
                        col._dataObsDate.append(x.exposure.obsdate)
                    elif col.table == db.Tables.STAR:
                        col.data.append(x.star.getField(col.field))
                        col._dataObsDate.append(x.exposure.obsdate)
                    elif col.table == db.Tables.EXPOSURE:
                        col.data.append(x.exposure.getField(col.field))
                        col._dataObsDate.append(x.exposure.obsdate)
                elif self.base == db.Tables.EXPOSURE:
                    if col.table == db.Tables.EXPOSURE:
                        col.data.append(x.getField(col.field))
                        col._dataObsDate.append(x.obsdate)
                elif self.base == db.Tables.STAR:
                    if col.table == db.Tables.STAR:
                        col.data.append(x.getField(col.field))

    def createTable(self, title, filename = None):
        table = []
        lastTime = None

        table.append([title])

        row = []
        for c in self.columns:
            row.append(c.name)
        table.append(row)
        
        done = 0
        while done < len(self.columns):
            row = []
            for c in self.columns:
                if c._dataIdx == len(c.data):
                    done += 1
                    row.append("")
                elif c._dataIdx > len(c.data):
                    row.append("")
                elif (self.base == db.Tables.EXPSTAR) or (self.base == db.Tables.EXPOSURE):
                    if (lastTime == None) or (c._dataObsDate[c._dataIdx] >= lastTime):
                        row.append(str(c.data[c._dataIdx]))
                        lastTime = c._dataObsDate[c._dataIdx]
                        c._dataIdx += 1
                    else:
                        row.append("")
                elif self.base == db.Tables.STAR:
                    row.append(str(c.data[c._dataIdx]))
                    c._dataIdx += 1
                else:
                    row.append("")
            table.append(row)
            lastTime = None

        if filename == None:
            return table
        else:
            file = open(filename, "w")
            for r in table:
                print >> file, "\t".join(r)
            file.flush()
            file.close()
            return filename

    def createHTMLTable(self, title, filename = None):
        if filename != None:
            table = self.createTable(title, None)
            file = open(filename, "w")
            print >> file, "<html><head><title>", title, "</title></head><body>"
            print >> file, "<h1>", title, "</h1><table width=\"100%\" border=1>"
            i = 0
            for row in table:
                if i == 0:
                    pass
                else:
                    print >> file, "<tr>"
                    for cell in row:
                        if i == 1:
                            print >> file, "<td><b>", cell, "</b></td>"
                        else:
                            print >> file, "<td>", cell, "</td>"
                    print >> file, "</tr>"
                i += 1
            print >> file, "</table></body></html>"
            file.flush()
            file.close()

    def createPlot(self, title, filename = None):
        # List of point markers
        markers = ['ro', 'go', 'bo', 'co', 'mo', 'rs', 'gs', 'bs', 'cs', 'ms', 'rD', 'gD', 'bD', 'cD', 'mD']
        
        if len(self.columns) > 1:
            # Clear figure
            plt.figure()

            # For every pair of columns
            for i in range(len(self.columns) / 2):
                # Get columns
                xdata = self.columns[i * 2].data
                ydata = self.columns[i * 2 + 1].data

                # Skip if either column is empty
                if len(xdata) > 0 and len(ydata) > 0:
                    # Check if x-axis uses dates
                    if isinstance(xdata[0], datetime):
                        plt.plot_date(xdata, ydata, markers[i], xdate = True, ydate = False)

                    # Check if y-axis uses dates
                    elif isinstance(ydata[0], datetime):
                        plt.plot_date(xdata, ydata, markers[i], xdate = False, ydate = True)

                    # Neither axis uses dates
                    else:
                        plt.plot(xdata, ydata, markers[i])

            # Get axis labels from first pair of columns
            plt.xlabel(self.columns[0].name)
            plt.ylabel(self.columns[1].name)

            # Set title
            plt.title(title)

            # Save to file
            if filename != None:
                plt.savefig(filename)
                return filename
        return None

    def resetColumns(self):
        self.columns = []

    def runScriptString(self, string, params = {}):
        return self.runScript(StringIO.StringIO(string), params)
    
    def runScript(self, file, params = {}):
        if isinstance(file, StringTypes):
            file = open(file, 'r')

        output = []
        lineNum = 1
        for line in file:
            # Remove white space
            line = line.strip(" \n\t")

            # Default to showing error messages
            hideErrors = False

            # Ignore blank lines and comment lines
            if not (len(line) == 0 or line.startswith('#')):
                # Suppress error messages if line starts with "!"
                if line.startswith("!"):
                    line = line[1:]
                    hideErrors = True

                # Replace parameters with their values
                for p in params: line = line.replace('%' + p, str(params[p]))

                # Split into command and args, removing white space
                words = [w.strip(" \t") for w in line.split(' ', 1)]
                args = [a.strip(" \t") for a in words[1].split(',')]

                # Use try block to catch exceptions and change them into a more usable format
                try:
                    # Find command and run it
                    if words[0] == "base":
                        self.setBase(args[0])
                    elif words[0] == "batch":
                        if args[1] == "exact":   self.batchExact(args[0], args[2], args[3])
                        elif args[1] == "error": self.batchError(args[0], args[2], args[3], args[4])
                        elif args[1] == "range": self.batchRange(args[0], args[2], args[3], args[4])
                        elif args[1] == "min":   self.batchMin(args[0], args[2], args[3])
                        elif args[1] == "max":   self.batchMax(args[0], args[2], args[3])
                        else:                    raise Exception("Keyword not found: %s" % (args[1]))
                    elif words[0] == "column":
                        self.addColumn(args[0], args[1])
                    elif words[0] == "filter":
                        if args[0] == "exact":   self.filterExact(args[1], args[2])
                        elif args[0] == "error": self.filterError(args[1], args[2], args[3])
                        elif args[0] == "range": self.filterRange(args[1], args[2], args[3])
                        elif args[0] == "min":   self.filterMin(args[1], args[2])
                        elif args[0] == "max":   self.filterMax(args[1], args[2])
                        else:                    raise Exception("Keyword not found: %s" % (args[1]))
                    elif words[0] == "output":
                        self.fillColumns()
                        if args[0] == "tabs":   self.createTable(args[1], args[2])
                        elif args[0] == "html": self.createHTMLTable(args[1], args[2])
                        elif args[0] == "plot": self.createPlot(args[1], args[2])
                        else:                   raise Exception("Keyword not found: %s" % (args[1]))
                        output.append(args[2])
                    elif words[0] == "reset":
                        if args[0] == "columns": self.resetColumns()
                        else:                    raise Exception("Keyword not found: %s" % (args[0]))
                    else: raise Exception("Keyword not found: %s" % (words[0]))
                except Exception, e:
                    # Display error message if error messages are not suppressed
                    if not hideErrors:
                        raise Exception("Error occurred in line %d: \"%s\"\n%s" % (lineNum, line, e))
            lineNum += 1
        return output
